fix(runtime)!: a mounted domain with no implementation answers 501, not a 404 that blames the route (#4093 follow-up) - #4222
Merged
Conversation
…ot a 404 that blames the route (#4093 follow-up) Two different facts were being answered with whatever each domain happened to reach for, and only `mcp` told them apart: - The route is not there. `/mcp` when the server is disabled for the environment; `/analytics` when the service is unserveable, because dispatcher-plugin gates the MOUNT and never registers those paths (#4000). A path the server does not expose is a 404 from the host's own router. Unchanged — that half was already right. - The route is there; the implementation is not. Every unconditionally mounted domain. The request reached a handler with nothing to delegate to. That is 501, and it is what changes here. `/automation` and `/notifications` returned `{ handled: false }`. That looks neutral, but it is not a fall-through: the dispatcher plugin's single exit turns it into `404 ROUTE_NOT_FOUND` with the hint "No handler matched this request. Check the API discovery endpoint for available routes." Both halves are false. A handler DID match — it just had nothing behind it — and discovery correctly does not list the route, so the hint points at a page that will never mention it. An operator reads that as a routing bug and goes looking for one that does not exist. `/ui` answered 503, which claims the condition is temporary. An uninstalled MetadataPlugin does not become installed by retrying. `/ai` answered 404 for the same mounted-but-unimplemented case. The refusal is written once (`domains/unavailable.ts`) and carries the same remedy sentence discovery reports for the slot, via `serviceUnavailableMessage` from the table #4204 added — so the wall and the discovery entry cannot drift into naming different fixes. `POST /api/v1/automation` on a stack without the service now answers `501 Install @objectstack/service-automation to enable`. `/ai` keeps a local message deliberately: its real provider ships outside this workspace as a Cloud/EE package, so CORE_SERVICE_PROVIDER — verified against workspace packages by check:service-providers — records `null` for that slot and the shared sentence would say "nothing ships", which is wrong. That gap between "no workspace package" and "no package" is worth knowing about; the override keeps this change honest without widening the guard's contract here. Deliberately unchanged: analytics's route-mount gate (a genuinely absent path); mcp's 404-vs-501 pair, which is the model this generalizes; the `handled: false` at the END of each domain, which means "no sub-route matched" and is a true 404; `GET /ai/agents`'s empty-list 200 (a deliberate courtesy for the console's per-navigation poll); and `/ai`'s `503 routes not yet initialized`, a different condition — service present, internal state unready — that may genuinely be transient. Tests: nine pinned the old answers and are updated to the new one, with the part that actually mattered preserved in each — the automation stub is still asserted NEVER CALLED, the notifications stub's `listInbox` likewise, and `/ai/agents` still returns its empty list. route-parity's integration assertion keeps its invariant (not advertised ⟹ not served, #3369) and now states why 501 expresses it better than the 404 it used to assert. Verified: full `pnpm test` 132/132 tasks; runtime 955; build 71/71; pnpm lint and every check:* gate clean (route-envelope needed the new domains/ file declared as handBuilt: 0 — it answers only through deps.error, which is that check's own thesis applied to the refusal itself). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The drift check flagged 18 docs — the blast radius of touching runtime — and none of them claimed the statuses this PR changes. But one already documented the rule for a single surface: `/graphql` is described as "route is wired but bring-your-own service: returns 501 unless an implementation is registered". That was the house convention all along; this PR makes it hold everywhere, so the API docs should state it as a rule rather than as a GraphQL quirk. Added to the Discovery section, where a reader asks the question it answers: 404 means the route is not mounted (analytics' gated mount, mcp when disabled), 501 means it is mounted with nothing behind it, the 501 body names the package to install, and nothing answers 503 for a missing capability — that status is for genuinely transient states like a still-booting kernel on /ready. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
os-zhuang
marked this pull request as ready for review
July 30, 2026 16:42
os-zhuang
pushed a commit
that referenced
this pull request
Jul 30, 2026
Picks up #4222, which rewrote the sibling refusal in `domains/ai.ts` (mounted-but-unimplemented now answers 501, not 404) and re-pinned `check-route-envelope.mjs`. It deliberately left `GET /ai/agents`'s empty-list 200 alone, so this PR's comment block on that branch merges without conflict — verifying it against the merged tree rather than assuming. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZaiYNM5igzGk6JiibjShb
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
#4093 follow-up) (#4301) CORE_SERVICE_PROVIDER recorded `null` for `ai` because no WORKSPACE package provides it, so `serviceUnavailableMessage('ai')` said "No implementation ships for the 'ai' slot". That is false — `@objectstack/service-ai` registers it — and it is the same class of wrong answer this table was introduced to end, one step further out: "not in this repository" was conflated with "does not exist". How it got in is worth naming. The claim that ai's provider is a Cloud/EE package was true, but it was asserted from a doc line and an old code comment without checking — exactly the provenance that made `plugin-search` and `plugin-workflow` wrong in the first place. Verified properly now, against the cloud repository: `packages/service-ai/src/plugin.ts` calls `ctx.registerService('ai', …)`, and the package is `private: true`. So something ships AND there is nothing to install — which is why `null` stays correct and why the sentence still must not start with "Install". `search`, `workflow` and `graphql` were re-checked the same way; nothing in either repository registers them, so their `null` and their "nothing ships" sentence are accurate on double-sided evidence now. `ai` gets a REMEDY_DETAIL entry — the mechanism `ui` already used for a slot the simple sentence describes badly. Discovery (`services.ai.message`) and the /ai 501 body both report it. This REMOVES code. `/ai`'s domain carried a local message override precisely because the shared sentence was wrong there; correcting the table fixed the domain AND discovery, which an override could never reach. The override and `capabilityUnavailable`'s `message?` parameter are both gone, and the helper documents why there is no per-call escape hatch: a slot whose sentence is wrong needs the table corrected. check-service-providers keeps its workspace-only rule — it cannot see another repository, and widening it is not the fix — but it no longer repeats the conflation in its own output, and its failure text now tells the next person what to do in exactly this case: `null` plus a REMEDY_DETAIL sentence when something ships that simply cannot be installed. Verified by pointing the table at the private package and reading the error. Also corrected four stale claims about /ai's absent-service answer: three calling it a 404 (docs/api/client-sdk.mdx, docs/releases/v17.mdx, a comment in packages/client's URL-conformance test), stale since #4222 made it 501; and the discovery sample in docs/protocol/kernel/http-protocol.mdx, which quoted the very sentence this change corrects. Verified: full `pnpm test` 132/132 tasks; runtime 954; spec's provider suite 8; build 71/71; pnpm lint, every check:* gate and spec's api-surface clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#4204 统一了 discovery 说的**"该装什么";这个 PR 统一"答什么码"** —— 空能力应答面上最后一块。
两个事实,一直被混在一起答
代码里其实早就有正确的区分,只是只有
mcp一个域贯彻了:/mcp环境未启用;/analytics服务不可服务时 dispatcher-plugin 门控挂载、根本不注册这些路径(#4000)各自为政的三种形态
/automation、/notifications返回{ handled: false }—— 看着像"中立地让位",但它不是让位:dispatcher plugin 的唯一出口把它变成404 ROUTE_NOT_FOUND,附带提示 "No handler matched this request. Check the API discovery endpoint for available routes."这句话两半都是假的:handler 确实匹配了(只是背后没实现),而 discovery 正确地不列这条路由 —— 提示把人指向一个永远不会提到它的页面。运维读到这个会去找一个不存在的路由 bug。
/ui答 503 —— 503 宣称"暂时不可用,稍后重试",但没装 MetadataPlugin 不会因为重试就装上。/ai答 404 —— 同一类。改法
拒绝只写一次(
domains/unavailable.ts),并且携带 discovery 为该槽位报告的同一句补救指引(复用 #4204 的serviceUnavailableMessage)—— 于是"墙"和"discovery 条目"不可能各说各的:/ai刻意保留本地文案:它真正的提供者@objectstack/service-ai作为 Cloud/EE 包在这个 workspace 之外,而CORE_SERVICE_PROVIDER由check:service-providers按 workspace 包校验、因此记为null,共享句会说"没有实现可装" —— 那是错的。"没有 workspace 包"≠"没有包" 这个缺口值得记下来;这里用覆盖参数保持诚实,不顺手放宽守卫的契约。刻意不动
analytics的挂载门 —— 路径是真的不存在mcp的 404/501 双答 —— 它就是本 PR 推广的那个模型handled: false—— 那是"子路径没匹配上",真 404GET /ai/agents的空列表 200 —— console 每次导航都轮询,刻意的礼遇/ai的503 routes not yet initialized—— 不同条件(服务在、内部状态未就绪),可能真的是暂时的测试
9 条钉旧应答的已更新,每条最要紧的断言都保住了:automation stub 仍断言从未被调用、notifications 的
listInbox同样、/ai/agents仍返回空列表。route-parity 的集成断言保留原不变量(不广告 ⟹ 不提供,#3369),并写清 501 为何比它原来断言的 404 更好地表达了它。验证
全仓
pnpm test132/132;runtime 955;build 71/71;pnpm lint与全部check:*干净 —— route-envelope 要求新增的domains/文件登记为handBuilt: 0,已登记(它只经deps.error作答,正是该检查自身的主张应用在"拒绝"本身上)。🤖 Generated with Claude Code
https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
Generated by Claude Code